﻿//*** Guidance generated code ***//
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Text;
using System.Web.Mvc;
using System.Web.Routing;
using System.Web.UI;

using ProjectBase.Core;
using ProjectBase.Core.Service;
using ProjectBase.Core.Web;
using $safeprojectname$;
using $safeprojectname$.Views;

namespace $safeprojectname$.Controllers
{
    /// <summary>
    ///     Controller base class for basic read operation
    ///     (filter, List, Detail)
    ///     and extending features like paging, sorting etc.
    /// </summary>
    /// <typeparam name = "T">Entity with int ID to be moved between Controller and View</typeparam>
    /// <typeparam name = "TModel">Type of the object available via Model property - lazily (when firstly needed) instantiated</typeparam>
    /// <typeparam name = "TFacade">Read/Entity Facade available via Facade property  - lazily (when firstly needed) instantiated</typeparam>
    /// <typeparam name = "TFilter"></typeparam>
    [OutputCache(NoStore = true, Duration = 0, VaryByParam = "*")]
    public abstract class StringReadController<T, TFilter, TModel, TFacade>
        : WebController<TModel>, IWebController<TModel>
        where T : class, IPersistentObjectWithTypedId<string>
        where TModel : class, IEditModel<T, string, TFilter>
        where TFacade : class, IReadFacade<T, string, TFilter>
        where TFilter : class, IFilter<string>
    {
        #region init

        protected override void Initialize(RequestContext requestContext)
        {
            base.Initialize(requestContext);
            Model.Filter = ClearFilter();
        }

        #endregion init

        #region properties Facade & Model

        /// <summary>
        ///     Lazily loaded (if needed) Facade of type TFacade.
        ///     Facade public setter allows DI.
        ///     Getter serves as the gate to the middle tier,
        ///     where the business rules are applied.
        /// </summary>
        public virtual TFacade Facade { protected get; set; }

        #endregion properties Facade & Model

        #region Index

        /// <summary>
        ///     The Gate to the controller.
        ///     Te Entity controllers are transfered to the 'List()' action
        ///     unless changed (e.g. to filter as a starting point for every numerous entity)
        /// </summary>
        /// <returns></returns>
        public override ActionResult Index()
        {
            return List();
        }
        #endregion Index

        #region filter

        /// <summary>
        ///     Navigates to filtering screen.
        ///     Main action "List" will in the next step collect the filtering values
        ///     and call Facade for the paged list view
        /// </summary>
        /// <returns></returns>
        public virtual ActionResult Find()
        {
            OnBeforeFind();
            OnBeforeAny();

            return View(Constants.Actions.Find, Model);
        }

        #endregion filter

        #region List

        /// <summary>
        ///     Collects the filter parameters (filled on filter screen or stored in the session)
        ///     to be used as criteria for 'Entity' Facade Find()
        /// </summary>
        /// <returns>List view filled with returned values</returns>
        [HttpPost]
        public virtual ActionResult Search(int? page = null)
        {
            OnBeforeList();
            OnBeforeAny();

            Model.Filter.CurrentPage = page ?? Model.Filter.CurrentPage;
            Model.Filter = OnBindFilter();

            if (Model.Messages.ContainsError())
            {
                return RedirectToAction(Constants.Actions.Find);
            }

            Model.ListModel.TargetNameForLink = Model.Filter.TargetNameForLink; // set the Target for Links
            Model.ListModel.ItemsToDisplay = LoadItemsToDisplay(Model.Filter).ToList();

            if (Model.Messages.ContainsError())
            {
                return RedirectToAction(Constants.Actions.Find);
            }

            FillItemPositionInList();

            AddClearButton();

            TempData["model"] = Model;
            return RedirectToAction("List", new { page = (Model.Filter.CurrentPage + 1) });
        }

        public virtual ActionResult List(int? page = null)
        {
            var model = TempData["model"];

            return model.IsNull()
                ? Search(page: (page.HasValue ? page - 1 : null))
                : View(Constants.Actions.List, model);
        }

        #endregion List

        #region Detail

        public virtual ActionResult Detail(string id)
        {
            if (id.IsEmpty())
            {
                return RedirectToAction(Constants.Actions.Find);
            }
            Model.Item = Facade.GetById(id);
            if (Model.Item.IsNull())
            {
                this.PublishWarning(Constants.Messages.EntityNotFound2
                    , ControllerName.GetLocalized()
                    , id);
                return RedirectToAction(Constants.Actions.Find);
            }

            OnBeforeDetail(id);
            OnBeforeAny();

            return View(Constants.Actions.Detail, Model);
        }

        #endregion Detail

        #region Cancel Clear

        /// <summary>
        ///     This action always provides direct Redirection to the List action
        /// </summary>
        /// <returns></returns>
        public virtual ActionResult Cancel()
        {
            return RedirectToAction(Constants.Actions.EntityDefault);
        }

        /// <summary>
        ///     This action clears Searching Parameters to default values.
        ///     Results in redirection to the filter
        /// </summary>
        public virtual ActionResult Clear(string target)
        {
            Model.Filter = ClearFilter();
            if (target.IsNotEmpty()
                && !target.Equals(Constants.Actions.NoView.Clear))
            {
                return RedirectToAction(target);
            }
            return RedirectToAction(Constants.Actions.Find);
        }

        #endregion Cancel Clear

        #region SortList

        public virtual ActionResult Asc(string orderProperty = null)
        {
            var originalState = Model.Filter.ToByteArray(); // to know if there were any changes

            if (orderProperty.IsNotEmpty())
            {
                Model.Filter.OrderBy.Clear();
                Model.Filter.OrderBy[orderProperty] = true;
            }

            Model.Filter.HasDefaultValues &= originalState.Compare(Model.Filter.ToByteArray());

            return RedirectToAction(Constants.Actions.List, this.QueryStringToDictionary());
        }

        public virtual ActionResult Desc(string orderProperty = null)
        {
            var originalState = Model.Filter.ToByteArray(); // to know if there were any changes

            if (orderProperty.IsNotEmpty())
            {
                Model.Filter.OrderBy.Clear();
                Model.Filter.OrderBy[orderProperty] = false;
            }

            Model.Filter.HasDefaultValues &= originalState.Compare(Model.Filter.ToByteArray());

            return RedirectToAction(Constants.Actions.List, this.QueryStringToDictionary());
        }

        #endregion SortList

        #region MoveNext MovePrevious

        /// <summary>
        ///     Returns the detail view of the next Item
        /// </summary>
        /// <param name = "id"></param>
        /// <returns></returns>
        public virtual ActionResult MovePrev(string id)
        {
            if (id.IsEmpty())
            {
                return RedirectToAction(Constants.Actions.Find);
            }
            if (Model.Filter.ItemPositionInList.IsNull()
                || Model.Filter.ItemPositionInList.Count == 0
                || !Model.Filter.ItemPositionInList.Contains(id))
            {
                FillItemPositionInList();
            }
            Model.Filter.CurrentRow = Model.Filter.ItemPositionInList.IndexOf(id) - 1;
            if (Model.Filter.CurrentRow == -2)
            {
                return RedirectToAction(Constants.Actions.Detail, new { id });
            }
            if (Model.Filter.CurrentRow == -1)
            {
                if (Model.Filter.CurrentPage == 0)
                {
                    Model.Filter.CurrentRow++;
                }
                else
                {
                    Model.Filter.CurrentPage--;
                    FillItemPositionInList();
                    Model.Filter.CurrentRow = Model.Filter.ItemPositionInList.Count - 1;
                }
            }
            return RedirectToAction(Constants.Actions.Detail,
                new { id = Model.Filter.ItemPositionInList[Model.Filter.CurrentRow] });
        }

        /// <summary>
        ///     Returns the detail view of the next Item
        /// </summary>
        /// <param name = "id"></param>
        /// <returns></returns>
        public virtual ActionResult MoveNext(string id)
        {
            if (id.IsEmpty())
            {
                return RedirectToAction(Constants.Actions.Find);
            }
            if (Model.Filter.ItemPositionInList.IsNull()
                || Model.Filter.ItemPositionInList.Count == 0
                || !Model.Filter.ItemPositionInList.Contains(id))
            {
                FillItemPositionInList();
            }
            Model.Filter.CurrentRow = Model.Filter.ItemPositionInList.IndexOf(id) + 1;
            if (Model.Filter.CurrentRow == 0)
            {
                return RedirectToAction(Constants.Actions.Detail, new { id });
            }
            if (Model.Filter.CurrentRow == Model.Filter.ItemPositionInList.Count)
            {
                if (Model.Filter.CurrentPage == Model.Filter.PageCount)
                {
                    Model.Filter.CurrentRow--;
                }
                else
                {
                    Model.Filter.CurrentPage++;
                    FillItemPositionInList();
                    Model.Filter.CurrentRow = 0;
                }
            }
            return RedirectToAction(Constants.Actions.Detail,
                new { id = Model.Filter.ItemPositionInList[Model.Filter.CurrentRow] });
        }

        /// <summary>
        ///     Keeps the list of IDs of currently selected items.
        ///     This supports finding out position of current item and paging up and down
        /// </summary>
        protected virtual void FillItemPositionInList()
        {
            if (Model.ListModel.ItemsToDisplay.Is())
            {
                Model.Filter.ItemPositionInList = Model.ListModel.ItemsToDisplay
                    .Select(i => i.ID)
                    .ToList();
            }
            else
            {
                var filter = Model.Filter.Clone() as TFilter;
                filter.AddPropertyToProjectionList(() => Model.Item.ID);

                Model.Filter.ItemPositionInList = Facade
                    .Find(filter)
                    .Select(i => i.ID)
                    .ToList();
            }
        }

        #endregion MoveNext MovePrevious

        #region ListToExcel

        /// <summary>
        ///     Returns the
        /// </summary>
        /// <returns></returns>
        public virtual ActionResult ListToExcel()
        {
            var maxRows = Model.Filter.TakeRows;

            var model = Factory.CreateInstance<IExportModel>();

            OnBeforeListToExcel(model);

            model.DataOutput = OnListToExcel(Model.Filter);

            Model.Filter.TakeRows = maxRows;

            return model.DataOutput.Is()
                ? new FileCoreResult(model) as ActionResult
                : RedirectToAction(Constants.Actions.List);
        }

        #endregion ListToExcel

        #region Async List

        public virtual ActionResult AsyncList(int? page = null, string sort = null, bool? direction = null, bool showRemoveAction = false)
        {
            var filter = ClearFilter();

            filter = OnBindFilter(filter);

            filter.CurrentPage = page ?? 0;

            if (sort.IsNotEmpty())
            {
                filter.OrderBy.Clear();
                filter.OrderBy[sort] = !(direction ?? false);
            }

            Model.ListModel.ItemsToDisplay = LoadAsyncItemsToDisplay(filter);

            var table = new JTable();

            SetTable(Constants.Actions.Special.AsyncList, table, filter, Model.ListModel.ItemsToDisplay, showRemoveAction);

            return new JsonCoreResult<JTable>(table);
        }

        #endregion Async List

        #region AutoComplete

        public virtual ActionResult AutoComplete(string term)
        {
            var filter = ClearFilter();
            filter.TakeRows = 5;

            Facade.BindFilter(filter, ValueProvider.ToContainer());
            filter.TakeRows = Math.Min(filter.TakeRows, 20);

            OnAutoCompleteList(filter, term);

            var completion = new JCompletion();

            foreach (var item in Model.ListModel.ItemsToDisplay)
            {
                completion.Options
                    .Add(new JOption
                    {
                        Value = item.ID,
                        Label = item.Description,
                    });
            }

            return new JsonCoreResult<JCompletion>(completion);
        }

        #endregion AutoComplete

        #region ValidationRules

        //[OutputCache(NoStore = false, Duration = 60, VaryByParam = "*", Location = OutputCacheLocation.Any)]
        public virtual ActionResult ValidationRules(string filter)
        {
            return JavaScript(Facade.ValidationRulesToString(filter));
        }

        #endregion ValidationRules

        #region Async ComboBox (ajax)

        /// <summary>
        ///     Returns the filtered list of Entities (url filtering parameters can be used)
        ///     The list is rendered as ComboBox.
        /// </summary>
        /// <returns>rendered ComboBox</returns>
        public virtual ActionResult ComboBox()
        {
            var model = Factory.CreateInstance<IComboBoxModel>();

            BindModel(model);

            model.ComboBoxName = model.ComboBoxName.IsNotEmpty()
                                     ? model.ComboBoxName
                                     : ControllerName;

            var filter = CreateComboBoxFilter();

            Facade.BindFilter(filter, ValueProvider.ToContainer());

            model.Items = new SelectList(Facade.Find(filter)
                , Constants.Common.ID
                , Constants.Common.Display
                , model.SelectedID);

            return View(Constants.WebControls.ComboBox, model);
        }

        /// <summary>
        ///   Could be overriden to define custome filter for ComboBox
        /// </summary>
        /// <returns></returns>
        protected virtual TFilter CreateComboBoxFilter()
        {
            return ClearFilter();
        }
        #endregion Async ComboBox (ajax)

        #region protected OnBind

        /// <summary>
        ///     In derived controller could make another Item-binding then ValueType properties.
        ///     Base 1) compares if there are any changes in the Model.FilterParam
        ///     to append Clear button
        ///     2) Resolves the current page from the Query string
        /// </summary>
        /// <returns></returns>
        protected virtual TFilter OnBindFilter(TFilter filterToBind = null)
        {
            var filter = filterToBind ?? Model.Filter;

            var originalState = filter.ToByteArray();

            filter = Facade.BindFilter(filter, ValueProvider.ToContainer());

            BindModel(Model.ListModel, includeProperties: new[] { "HidePagingButtons", "RowRenderMode" });

            filter.HasDefaultValues &= originalState.Compare(filter.ToByteArray());

            return filter;
        }

        #endregion protected OnBind

        #region protected OnList

        /// <summary>
        ///     This is the default method, which will create
        ///     the 'displayable-data' representing Items.
        ///     Item's ToDisplay() method is used, and sorting is switched off.
        ///     This method should be overriden
        /// </summary>
        protected virtual IEnumerable<IItemToDisplay> LoadItemsToDisplay(TFilter filter)
        {
            return Facade.Find(filter)
                .Select(i => new ItemToDisplay
                {
                    ID = i.IDToDisplay,
                    Description = i.ToDisplay(),
                    Items = new List<ICellDescription>
                    {
                        new CellDescription {HeaderName = Constants.Common.Description, Value = i.ToDisplay(), Sort = false},
                        i.Cell(e => e.ID, new CellDescription {Align = Align.right, Format = "N0"}),
                    }
                } as IItemToDisplay);
        }

        /// <summary>
        ///     Provides data for Excel.
        /// </summary>
        protected virtual IEnumerable<byte> OnListToExcel(TFilter filter)
        {
            return ExportToOpenXmlSpreadsheet
                (
                    "UserName",
                    "Organization".GetLocalized(),
                    LoadExcelItemsToDisplay(filter).ToList()
                );
        }

        protected virtual IEnumerable<byte> ExportToOpenXmlSpreadsheet(string userName
            , string organization
            , IEnumerable<IEnumerable<IExCell>> entities)
        {
            var exporter = Factory.CreateInstance<Core.Tools.Export.IExcelExporter>();
            var model = Factory.CreateInstance<Core.Tools.Export.IExcelExporterData>();

            model.Entities = entities;
            model.UserName = userName;
            model.Organization = organization;

            return exporter.Execute(model)
                ? model.OutputData
                : null;
        }

        protected abstract IEnumerable<IEnumerable<IExCell>> LoadExcelItemsToDisplay(TFilter filter);

        /// <summary>
        ///     Async list use this for generating the Model.ListModel.ItemsToDisplay collection
        ///     Could be overriden to provide different columns ...
        ///     Default implementation calls .LoadItemsToDisplay()'.
        /// </summary>
        protected virtual IEnumerable<IItemToDisplay> LoadAsyncItemsToDisplay(TFilter boundedSearch)
        {
            Model.ListModel.SetFilter(() => boundedSearch);
            return LoadItemsToDisplay(boundedSearch);
        }

        /// <summary>
        ///     Fills the Model.ListModel.ItemsToDisplay collection
        ///     for AutoComplete. (ICellDescription.ID and ICellDescription.Description are used
        ///     in base implementation of AutoComplete()
        /// </summary>
        /// <param name = "filter"></param>
        /// <param name = "term">The string sent from UI,
        ///     which could be used for filtering of the result (e.g. "j", "jq"...)</param>
        protected virtual void OnAutoCompleteList(TFilter filter, string term)
        {
            // ... use projection for columns needed to provide ID and Description:
            //filter.AddPropertyToProjectionList(() => Model.Item.ID);
            //filter.AddPropertyToProjectionList(() => Model.Item.Code);

            Model.ListModel.ItemsToDisplay = Facade.Find(filter)
                .Select(i => new ItemToDisplay
                {
                    ID = i.IDToDisplay,
                    Description = i.ToDisplay(),
                    Items = new List<ICellDescription>
                    {
                        // NOTE mostly ID and Description would serve for AutoComplete
                    }
                } as IItemToDisplay);
        }

        protected virtual void AddClearButton()
        {
            if (Model.Filter.HasDefaultValues)
            {
                return; // no need to render Clear Button on a ListView
            }
            this.AddButton(Constants.Actions.NoView.Clear, Url,
                new RouteValueDictionary { { Attr.Target, Constants.Actions.List } });
        }

        #endregion protected OnList

        #region protected OnBefore Actions

        /// <summary>
        ///     Sets Actions and buttons available on a filter screen
        /// </summary>
        protected virtual void OnBeforeFind()
        {
            Model.Filter.CurrentPage = 0;

            this.AddAction(Constants.Actions.Find, Url)
                .AddAction(Constants.Actions.List, Url);

            Model.MasterModel.ButtonSubmit = new ButtonItem(Constants.Actions.NoView.Search)
            {
                FormController = ControllerName,
            };

            this.AddButton(Constants.Actions.NoView.Clear, Url);

            Model.ValidationRulesJS = Url.Action(Constants.Actions.Special.ValidationRules
                , new { filter = Constants.Actions.Find });
        }

        /// <summary>
        ///     Sets Actions and buttons available on a List screen.
        ///     Common are New, filter, List and Expor to Excel
        /// </summary>
        protected virtual void OnBeforeList()
        {
            Model.MasterModel.CurrentAction = Constants.Actions.List;
            Model.ListModel.RowRenderMode = RowRenderMode.Readonly;

            this.AddAction(Constants.Actions.Find, Url)
                .AddAction(Constants.Actions.List, Url)
                .AddAction(Constants.Actions.ExportImport.ListToExcel, Url, Constants.Actions.Targets.NewWindow);
        }

        /// <summary>
        ///     Sets the Model.ExportModel to be ready for Excel generator
        /// </summary>
        protected virtual void OnBeforeListToExcel(IExportModel model)
        {
            Model.Filter.TakeRows = int.MaxValue;
            Model.Filter.ExportAllRows = true;

            model.ContentType = "application/excel";
            model.Encoding = Encoding.Unicode;
            model.FileName = ControllerName + "ExportFile" + DateTime.Now.Hour + DateTime.Now.Minute
                                         + DateTime.Now.Second;
            model.FileExtension = ".xlsx";
            model.ColumnSeparator = "|";
            model.XDocument = null;
        }

        /// <summary>
        ///     Sets Actions and buttons available on a Detail scrren
        /// </summary>
        protected virtual void OnBeforeDetail(string id)
        {
            this.AddAction(Constants.Actions.Find, Url)
                .AddAction(Constants.Actions.List, Url);

            if (Model.Filter.ItemPositionInList.Is()
                &&
                Model.Filter.ItemPositionInList.Contains(id))
            {
                Model.Filter.CurrentRow = Model.Filter.ItemPositionInList.IndexOf(id);
                this.AddAction(Constants.Actions.MovePrev, Url)
                    .AddAction(Constants.Actions.MoveNext, Url);
            }
            this.AddButton(Constants.Actions.NoView.CancelDefault, Url, text: Constants.Actions.NoView.CancelDetail);
        }

        /// <summary>
        ///     When overriden, this action is called on every OnBefore... action.
        ///     It allowes to append or remove actions for all Actions in one place
        /// </summary>
        protected virtual void OnBeforeAny()
        {
        }

        #endregion protected OnBefore Actions

        #region Model Store and Restore

        /// <summary>
        ///     Restores the Model from the Information stored in the storage (e.g. Session by default)
        /// </summary>
        /// <param name = "filterContext"></param>
        /// <param name = "storableModel"></param>
        protected override void RestoreModel(ActionExecutingContext filterContext, IStateObject storableModel)
        {
            var controller = filterContext.Controller as StringReadController<T, TFilter, TModel, TFacade>;
            if (storableModel.Is()
                && controller.Is())
            {
                if (storableModel.FilterParam.Is())
                {
                    controller.Model.Filter = storableModel.FilterParam as TFilter;
                }

                this.PublishDebug(
                    "OnActionExecuting: Restored FilterParam");
            }
            this.PublishDebug("OnActionExecuting: Finished RestoreModel from SessionStorage() ");
        }

        /// <summary>
        ///     Stores needed Model's properties in the storage (e.g. session by default)
        ///     to be available on return to this controller
        /// </summary>
        /// <param name = "filterContext"></param>
        /// <param name = "storableModel"></param>
        protected override void StoreModel(ActionExecutedContext filterContext, IStateObject storableModel)
        {
            var controller = filterContext.Controller as IWebController<IEntityModel<IPersistentObject, IFilter>>;

            if (controller.Is())
            {
                storableModel.FilterParam = controller.Model.Filter;
                storableModel.FilterParam.ProjectionList = null;

                #region Log Debug

                this.PublishDebug(
                    "OnActionExecuted: into SessionStorage() were placed:  FilterParam ");

                #endregion Log Debug
            }

            #region Log Debug

            this.PublishDebug("OnActionExecuted: Started Storing Model into the SessionStorage() ");

            #endregion Log Debug
        }

        #endregion Model Store and Restore

        #region Clear filter

        /// <summary>
        ///     Resets (or pre-sets) the Searching parameters to default values (for MVC purposes).
        ///     (base calls Facade.CreateNewFilter() - to get new filter even TFilter without new() constraint).
        ///     This controller method is intended as a place for MVC default settings.
        ///     E.g. to preset order column, row count, or some filter value.
        /// </summary>
        protected virtual TFilter ClearFilter()
        {
            var filter = Facade.CreateNewFilter();
            filter.AllowRewind = true;
            return filter;
        }

        #endregion Clear filter
    }
}